package game.Engine;

import java.awt.Rectangle;
import java.awt.geom.Rectangle2D;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.LinkedBlockingQueue;

import javax.sound.midi.Sequence;
import javax.sound.midi.Sequencer;

import com.googlecode.cqengine.CQEngine;
import com.googlecode.cqengine.IndexedCollection;
import com.googlecode.cqengine.attribute.Attribute;
import com.googlecode.cqengine.index.hash.HashIndex;
import com.googlecode.cqengine.index.navigable.NavigableIndex;
import com.googlecode.cqengine.query.Query;
import com.googlecode.cqengine.resultset.ResultSet;

import game.MainController;
import game.Engine.Entity.Alien;
import game.Engine.Entity.Creature;
import game.Engine.Entity.Entity;
import game.Engine.Entity.Magic;
import game.Engine.Entity.Mouse;
import game.Engine.Entity.Player;
import game.Engine.Entity.PowerUp;
import game.Engine.Entity.Tile;
import game.Engine.Map.Map;
import game.Input.EventSound;
import game.Input.GameAction;
import game.display.Images;
import game.display.SoundController;
import game.sound.MidiPlayer;

import static com.googlecode.cqengine.query.QueryFactory.*;

public class GameEngine extends Thread {
	// Variables
	public Player player;
	public Mouse mouse;
	public Map currentMap;
	public IndexedCollection<Entity> entities;	
	
	public volatile boolean paused;

	private Queue<GameAction> buffer;
	private Queue<Entity> toAdd;
	
	private int currentAlienCount;
	public static boolean progress;

	/**
	 * constructor for the gameEngine
	 * @param map contains all the entities
	 */
	public GameEngine(Map map) {
		entities = CQEngine.newInstance(); //creates a new instance of an IndexedCollection
		//Begining of adding the attributes
		entities.addIndex(NavigableIndex.onAttribute(Entity.ENTITY_X));
		entities.addIndex(NavigableIndex.onAttribute(Entity.ENTITY_Y));
		entities.addIndex(NavigableIndex.onAttribute(Entity.ENTITY_Z));
		entities.addIndex(HashIndex.onAttribute(Entity.Entity_class));
		entities.addIndex(HashIndex.onAttribute(Entity.isPowerup));
		//ending of adding attributes
		mouse = new Mouse(0, 0, Images.CURSOR.image());
		entities.add(mouse);
		currentMap = map;
		buffer = new LinkedBlockingQueue<GameAction>(); //creates new Queue
		toAdd = new LinkedList<Entity>(); //creates a new Queue
		paused = false;
		loadEntitiesFromMap();			//loads the entities on the maps
	}

	private void loadEntitiesFromMap() {
		for (Entity[] p : currentMap.entities) {
			for (Entity e : p) {
				if (e != null) {
					entities.add(e);
				}
				if (e instanceof Player) {
					player = (Player) e;
				}
				if (e instanceof Alien) {
					currentAlienCount++;
				}
			}
		}
	}

	/**
	 * returns the current count of aliens in the game
	 * @return the current count of aliens in the game
	 */
	public int getCurrentAlienCount() {
		return currentAlienCount;
	}
	
	/**
	 * adds 1 to the count of aliens when new alien spawns
	 */
	public void addAlienCount(){
		currentAlienCount++;
	}

	/**
	 * a query representing the question "what are all entities?"
	 */
	public Query<Entity> all = has(Entity.ENTITY_Z);

	/**
	 * a query representing the question "which entities are below e?"
	 * @param e the entity to find entities below
	 * @return a query representing the question "which entities are below e?"
	 */
	public Query<Entity> getBelow(Entity e) {
		return and(equal(Entity.Entity_class, Tile.class),
				between(Entity.ENTITY_X, e.getX() - 1, e.getX() + 1),
				between(Entity.ENTITY_Y, e.getY() + .5, e.getY() + 1.5));
	}

	/**
	 * a query representing the question "what are all powerups around this entity?"
	 * @param e the entity to find powerups around
	 * @return a query representing the question  "what are all powerups around this entity?"
	 */
	public Query<Entity> getPowerUpsAround(Entity e) {
		return and(equal(Entity.isPowerup, true),
				between(Entity.ENTITY_X, e.getX() - 1, e.getX() + 1),
				between(Entity.ENTITY_Y, e.getY() - 1, e.getY() + 1));
	}
	// Methods
	private void updateEntities(long elapsedTime) {
		Query<Entity> creatures = or(
				equal(Entity.Entity_class, Creature.class),
				equal(Entity.Entity_class, Player.class),
				equal(Entity.Entity_class, Alien.class));
		for (Entity e : entities.retrieve(creatures)) { // for each creature
			boolean nofall = nofall(e); //determine if the creature should fall
			if (!nofall) {
				((Creature) e).fall();
			} else {
				((Creature) e).setOnGround(true);
			}
		}
		for (Entity e : entities) { //for each entity
			e.update(elapsedTime); //update that entity
		}
		// for(Entity e: entities)
		for (Iterator<Entity> i = entities.iterator(); i.hasNext();) { //for each entity
			Entity e = i.next();
			if (e.getState() == Entity.State.DEAD) { //if its dead
				i.remove(); //remove it
				if (e instanceof Alien) { //if its an alien
					currentAlienCount--; //lower the count of aliens
					player.addPoints(((Alien) e).getPoints()); //add points to the player that killed it
				}
			}
		}
	}

	/**
	 * Determines if the entity should fall or not 
	 * @param e the entity in question
	 * @return true if the entity should not fall false otherwise
	 */
	public boolean nofall(Entity e) {
		int underX = (int) e.getX();
		int underY = (int) e.getY() + 1;
		int underRightX = (int) Math.ceil(e.getX());
		if ((underY < 0) || (underY > getMaxY())) { //if the entity is higher than the map, or under the map return false
			return false;
		}
		if (underX < 0) {
			if (underRightX < 0) {
				return false; //if the entity is off the left of the map return false
			} else {
				return currentMap.entities[(int) Math.ceil(e.getX())][(int) e
						.getY() + 1] instanceof Tile; //otherwise determine if there is a tile entity underneath the entity
			}
		}
		if (underX >= getMaxX()) {
			return false; //if the entity is off the right of the map return false
		} else {
			if (underRightX >= getMaxX()) {
				return (currentMap.entities[(int) e.getX()][(int) e.getY() + 1] instanceof Tile); //otherwise if the right of the entity is outside the map determin if there is a tile underneath the left of the entity 
			} else {
				return ((currentMap.entities[(int) e.getX()][(int) e.getY() + 1] instanceof Tile) || (currentMap.entities[(int) Math
						.ceil(e.getX())][(int) e.getY() + 1] instanceof Tile)); //otherwise determine if there is a tile underneath either side of the entity.
			}
		}
	}
	/**
	 * returns the entites that fit the input Query
	 * @param q the query to be run on the collection of entities
	 * @return all entiteis that fit q
	 */
	public ResultSet<Entity> runQuery(Query q) {
		return entities.retrieve(q);

	}
	/**
	 * returns the entities that fit the input Query, orders them by an Attribute
	 * @param q the query to be run
	 * @param orderby the Attribute to be ordered by
	 * @return an ordered collection of Entities based off of q and orderby
	 */
	public ResultSet<Entity> runQuery(Query<Entity> q, Attribute orderby) {
		return entities.retrieve(q, queryOptions(orderBy(orderby)));

	}
	/**
	 * returns a Query that represents the question "what entities are near this entity withing a distance of x?"
	 * @param e the entity to find other entities around
	 * @param x the distance to find them in
	 * @return a Query that represents the question "what entites are near e withing a distance of x?"
	 */
	public Query<Entity> getNear(Entity e, double x) {
		double min = Math.min((int) e.getX() - 1, x);
		double max = Math.max((int) e.getX() + 1.5, x + 1);
		return and(equal(Entity.Entity_class, Tile.class),
				between(Entity.ENTITY_X, min, max));
	}
	/**
	 * a Query that represents the question "What entities are within this rectangle?"
	 * @param r the rectangle to find entities within.
	 * @return a query that represents the questiong "What entities are within Rectangle r?"
	 */
	public Query<Entity> getwithin(Rectangle2D.Double r) {
		return or(
				between(Entity.ENTITY_X, r.x / 64.0 - 2,
						(r.x + r.width) / 64.0 + 2),
				between(Entity.ENTITY_Y, r.y / 64.0 - 2,
						(r.y + r.height) / 64.0 + 2));
	}

	private void executeBufferedActions() {
		while (!buffer.isEmpty()) {
			executeAction(buffer.poll());
		}
	}
	

	private void executeAction(GameAction action) {
		switch (action) {
		case PLAYER_UP:
			if (player.isOnGround())
				player.jump(false);
			else
				player.jump(true);
			break;
		case PLAYER_DOWN:
			player.slide();
			break;
		case PLAYER_LEFT:
			player.setAcceleratingLeft(true);
			break;
		case PLAYER_RIGHT:
			player.setAcceleratingRight(true);
			break;
		case PLAYER_LEFT_R:
			player.setAcceleratingLeft(false);
			break;
		case PLAYER_RIGHT_R:
			player.setAcceleratingRight(false);
			break;
		case PLAYER_DOWN_R:
			player.standUp();
			break;
		case PLAYER_MAGIC_WHITE:
			player.setWhite();
			break;
		case PLAYER_MAGIC_RED:
			player.setRed();
			break;
		case PLAYER_MAGIC_GREEN:
			player.setGreen();
			break;
		case PLAYER_MAGIC_BLUE:
			player.setBlue();
			break;
		case PLAYER_MAGIC_YELLOW:
			player.setYellow();
			break;
		case PLAYER_WHEEL_SCROLL_UP:
			player.changePlayerMagicUp();
			break;
		case PLAYER_WHEEL_SCROLL_DOWN:
			player.changePlayerMagicDown();
			break;
		case PLAYER_CAST:
			player.cast();
			break;
		default:
			break;
		}
	}
	/**
	 * adds a GameAction to the internal buffer
	 * @param gameAction the GameAction to be added
	 */
	public void addGameAction(GameAction gameAction) {
		buffer.add(gameAction);
	}
	
	private boolean running;
	/**
	 * Stops the game
	 */
	public void stopGame() {
		running = false;
	}
	/**
	 * runs the game
	 */
	public void run() {
		long lastTime = System.nanoTime();
		long elapsedTime;
		running = true;
		while (running) {
			elapsedTime = System.nanoTime() - lastTime;
			lastTime += elapsedTime; // find the time the calculations took
			if (paused) { //if the game is paused
				try {
					Thread.sleep(50); //sleep for at least 50 ms
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			} else {
				executeBufferedActions();//execute all buffered actions
				addBufferedEntities(); //add any buffered entities that need to be created
				MainController.soundController.executeEventSoundBufferedActions(); //execute any buffered sound events
				updateEntities(elapsedTime); //updates all entities with the elapsed time
				if (player.getState() == Entity.State.DEAD) { //checks if the player is dead
					MainController.soundController.stopMidi(); //to stop the sounds
					MainController.soundController.clearEventSoundBuffer();
					MainController
							.checkForHighScore(MainController.gameEngine.player
									.getTotalPoints()); //checks for a high score
					MainController.restart();//restarts from the main menu
					running = false;
				}
			}
		}
	}

	private void addBufferedEntities() {
		while (!toAdd.isEmpty()) {
			entities.add(toAdd.poll());
		}
	}
	/**
	 * adds an entity to the game
	 * @param e the entity to add
	 */
	public void addEntity(Entity e) {
		toAdd.add(e);
	}
	/**
	 * gets the maximum y of the map of this game
	 * @return the maximum y of the map of this game
	 */
	public int getMaxY() {
		int max = 0;
		for (Entity[] p : currentMap.entities) {
			if (p.length > max) {
				max = p.length;
			}
		}
		return max;
	}
	/**
	 * returns the maximum x of the map of this game
	 * @return the maximum x of the map of this game
	 */
	public int getMaxX() {
		return currentMap.entities.length;
	}
}
